﻿using System;
using Bookvar.ObjectModel;
using Bookvar.ObjectModel.Actions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bookvar.UnitTests.ObjectModel.Actions
{
    /// <summary>
    /// Tests for the <see cref="ChangeTopicAction"/> class.
    /// </summary>
    [TestClass]
    public class ChangeTopicActionTests : CommonActionTests
    {
        [TestMethod]
        public void CanCreateNew()
        {
            var action = new ChangeTopicAction();
            Assert.IsNotNull( action );
        }

        [TestMethod]
        [ExpectedException( typeof ( ArgumentNullException ) )]
        public void New_WithNullTopic_Throws()
        {
            new ChangeTopicAction( null );
        }

        [TestMethod]
        public void CanRedo_IsTrue()
        {
            var action = new ChangeTopicAction();

            Assert.IsTrue( action.CanRedo );
        }

        [TestMethod]
        public void CanUndo_IsTrue()
        {
            var action = new ChangeTopicAction();

            Assert.IsTrue( action.CanUndo );
        }

        [TestMethod]
        public void New_WithTargetTopic_SetsTheTargetTopic()
        {
            var topic = new Topic();
            var action = new ChangeTopicAction( topic );

            Assert.AreSame( topic, action.Topic );
        }

        [TestMethod]
        [ExpectedException( typeof ( InvalidOperationException ) )]
        public void Execute_WithNoTargetTopicSet_Throws()
        {
            var action = new ChangeTopicAction();
            action.Execute();
        }

        [TestMethod]
        public void Execute_WithNewName_ChangesTheNameOfTheTopic()
        {
            var topic = new Topic();
            var action = new ChangeTopicAction( topic ) { Name = "New" };
            action.Execute();

            Assert.AreEqual( "New", topic.Name );
        }

        [TestMethod]
        public void Undo_WithNewName_UndoesTheChangesToTheTopicName()
        {
            var topic = new Topic { Name = "Old" };
            var action = new ChangeTopicAction( topic ) { Name = "New" };
            action.Execute();
            action.Undo();

            Assert.AreEqual( "Old", topic.Name );
        }

        [TestMethod]
        public void Execute_WithNullNewName_DoesNotChangeTopicName()
        {
            var topic = new Topic { Name = "TestName" };
            var action = new ChangeTopicAction( topic ) { Name = null };
            action.Execute();

            Assert.AreEqual( "TestName", topic.Name );
        }

        [TestMethod]
        public void Execute_WithNewParent_ChangesTheTopicParent()
        {
            var newParent = new Topic();
            var oldParent = new Topic();
            var topic = new Topic { Parent = oldParent };
            var action = new ChangeTopicAction( topic ) { Parent = newParent };
            action.Execute();

            Assert.AreSame( newParent, topic.Parent );
            // validate these due to strange behaviour of the Topic.
            CollectionAssert.Contains( newParent.SubTopics, topic );
            CollectionAssert.DoesNotContain( oldParent.SubTopics, topic );
        }

        [TestMethod]
        public void Undo_WithNewParent_UndoesTheChangesToTheTopicParent()
        {
            var newParent = new Topic();
            var oldParent = new Topic();
            var topic = new Topic { Parent = oldParent };
            var action = new ChangeTopicAction( topic ) { Parent = newParent };
            action.Execute();
            action.Undo();

            Assert.AreSame( oldParent, topic.Parent );
            // validate these due to strange behaviour of the Topic.
            CollectionAssert.Contains( oldParent.SubTopics, topic );
            CollectionAssert.DoesNotContain( newParent.SubTopics, topic );
        }

        [TestMethod]
        [ExpectedException( typeof ( ArgumentNullException ) )]
        public void Execute_WithNullNewParent_DoesNotChangeTopicParent()
        {
            var parent = new Topic();
            var topic = new Topic { Parent = parent };
            var action = new ChangeTopicAction( topic ) { Parent = null };
            action.Execute();
        }

        [TestMethod]
        public void Undo_WithNewParentAndWithNullOldParent_UndoesTheParentToNull()
        {
            var topic = new Topic { Parent = null };
            var action = new ChangeTopicAction( topic ) { Parent = new Topic() };
            action.Execute();
            action.Undo();

            Assert.IsNull( topic.Parent );
        }

        [TestMethod]
        public void Execute_WithAttachmentsToAdd_AddsThemToTheTargetTopic()
        {
            var attachmentToAdd = new Attachment();
            var targetTopic = new Topic();

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToAdd.Add( attachmentToAdd );
            action.Execute();

            Assert.AreEqual( 1, targetTopic.Attachments.Count );
            Assert.AreEqual( attachmentToAdd, targetTopic.Attachments[ 0 ] );
        }

        [TestMethod]
        public void Undo_WithAttachmentsToAdd_RemovesTheAttachments()
        {
            var attachmentToAdd = new Attachment();
            var targetTopic = new Topic();
            targetTopic.Attachments.AddRange( new[] { new Attachment(), new Attachment() } );

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToAdd.Add( attachmentToAdd );
            action.Execute();
            action.Undo();

            CollectionAssert.DoesNotContain( targetTopic.Attachments, attachmentToAdd );
        }

        [TestMethod]
        public void Execute_WithAttachmentsToRemove_RemovesThemFromTheTargetTopic()
        {
            var attachmentToRemove = new Attachment();
            var targetTopic = new Topic();
            targetTopic.Attachments.Add( attachmentToRemove );
            targetTopic.Attachments.Add( new Attachment() );

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToRemove.Add( attachmentToRemove );
            action.Execute();

            CollectionAssert.DoesNotContain( targetTopic.Attachments, attachmentToRemove );
        }

        [TestMethod]
        public void Undo_WithAttachmentsToRemove_AddsBackTheAttachments()
        {
            var attachmentToRemove = new Attachment();
            var targetTopic = new Topic();
            targetTopic.Attachments.Add( attachmentToRemove );
            targetTopic.Attachments.Add( new Attachment() );

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToRemove.Add( attachmentToRemove );
            action.Execute();
            action.Undo();

            CollectionAssert.Contains( targetTopic.Attachments, attachmentToRemove );
        }

        [TestMethod]
        public void Execute_WhenSomeOfTheAttachmentsToRemoveAreNotInTheTargetTopic_RemovesOnlyThePresentOnes()
        {
            var notTopicAttachment = new Attachment();
            var topicAttachment = new Attachment();
            var targetTopic = new Topic();
            targetTopic.Attachments.Add( topicAttachment );

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToRemove.Add( topicAttachment );
            action.AttachmentsToRemove.Add( notTopicAttachment );
            action.Execute();

            CollectionAssert.DoesNotContain( targetTopic.Attachments, topicAttachment );
        }

        [TestMethod]
        public void
            Undo_WhenSomeOfTheAttachmentsToRemoveWereNotInTheTargetTopicOnExecute_AddsBackOnlyTheRemovedAttachments()
        {
            var notPresentAttachment = new Attachment();
            var presentAttachment = new Attachment();
            var targetTopic = new Topic();
            targetTopic.Attachments.Add( presentAttachment );

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToRemove.Add( presentAttachment );
            action.AttachmentsToRemove.Add( notPresentAttachment );
            action.Execute();
            action.Undo();

            CollectionAssert.DoesNotContain( targetTopic.Attachments, notPresentAttachment );
            CollectionAssert.Contains( targetTopic.Attachments, presentAttachment );
        }

        [TestMethod]
        public void Execute_WithCommonAttachmentsToAddAndRemove_FirstRemovesAndThenAdds()
        {
            var attachment = new Attachment();
            var targetTopic = new Topic();
            targetTopic.Attachments.Add( attachment );

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToRemove.Add( attachment );
            action.AttachmentsToAdd.Add( attachment );
            action.Execute();

            // if the attachment is first removed, then added - it should be in the topic.
            Assert.AreEqual( 1, targetTopic.Attachments.Count );
            Assert.AreSame( attachment, targetTopic.Attachments[ 0 ] );
        }

        [TestMethod]
        public void Undo_WithCommonAttachmentsToAddAndRemove_FirstUndoesTheAddAndThenTheRemove()
        {
            var attachment = new Attachment();
            var targetTopic = new Topic();
            targetTopic.Attachments.Add( attachment );

            var action = new ChangeTopicAction( targetTopic );
            action.AttachmentsToRemove.Add( attachment );
            action.AttachmentsToAdd.Add( attachment );
            action.Execute();
            action.Undo();

            // undoes first the add - the attachment is removed.
            // then undoes the remove - the attachment is added.
            Assert.AreEqual( 1, targetTopic.Attachments.Count );
            Assert.AreSame( attachment, targetTopic.Attachments[ 0 ] );
        }

        [TestMethod]
        public void Execute_WithNewHyperlink_ChangesTheHyperLinkTheTopic()
        {
            var topic = new Topic { Hyperlink = new Uri( "http://OLD.NET" ) };
            var newHyperLink = new Uri( "http://bookvar.net" );

            var action = new ChangeTopicAction( topic ) { Hyperlink = newHyperLink };
            action.Execute();

            Assert.AreEqual( newHyperLink, topic.Hyperlink );
        }

        [TestMethod]
        public void Undo_WithNewHyperlink_UndoesTheChangesToTheTopicHyperLink()
        {
            var oldHyperlink = new Uri( "http://OLD.NET" );
            var newHyperlink = new Uri( "http://bookvar.net" );
            var topic = new Topic { Hyperlink = oldHyperlink };

            var action = new ChangeTopicAction( topic ) { Hyperlink = newHyperlink };
            action.Execute();
            action.Undo();

            Assert.AreEqual( oldHyperlink, topic.Hyperlink );
        }

        [TestMethod]
        public void Execute_WithNullNewHyperLink_SetsTheTopicHyperlinkToNull()
        {
            // TODO: enable this test when the topic supports NULL hyperlink.
//            var topic = new Topic { Hyperlink = new Uri( "http://OLD.NET" ) };
//
//            var action = new ChangeTopicAction( topic ) { NewHyperlink = null };
//            action.Execute();
//
//            Assert.IsNull( topic.Hyperlink );
        }

        [TestMethod]
        public void Execute_WithResourcesToRemove_RemovesTheResourcesFromTheTopic()
        {
            var topic = new Topic();
            var resource = new Resource();
            topic.Resources.Add( resource );

            var action = new ChangeTopicAction( topic );
            action.ResourcesToRemove.Add( resource );
            action.Execute();

            Assert.IsTrue( topic.Resources.Count == 0 );
        }

        [TestMethod]
        public void Undo_WithResourcesToRemove_RevertsTheChanges()
        {
            var topic = new Topic();
            var resource = new Resource();
            topic.Resources.Add( resource );

            var action = new ChangeTopicAction( topic );
            action.ResourcesToRemove.Add( resource );
            action.Execute();
            action.Undo();

            Assert.IsTrue( topic.Resources.Count == 1 );
            Assert.AreSame( topic.Resources[ 0 ], resource );
        }

        [TestMethod]
        public void Execute_WithResourcesToAdd_AddsTheResourcesToTheTopic()
        {
            var topic = new Topic();
            var resource = new Resource();

            var action = new ChangeTopicAction( topic );
            action.ResourcesToAdd.Add( resource );
            action.Execute();

            Assert.IsTrue( topic.Resources.Count == 1 );
            Assert.AreSame( topic.Resources[ 0 ], resource );
        }

        [TestMethod]
        public void Undo_WithResourcesToAdd_RevertsTheChanges()
        {
            var topic = new Topic();
            var resource = new Resource();

            var action = new ChangeTopicAction( topic );
            action.ResourcesToAdd.Add( resource );
            action.Execute();
            action.Undo();

            Assert.IsTrue( topic.Resources.Count == 0 );
        }
        
        // TODO: add tests when Topic.Notes is no longer text range.
//        [TestMethod]
//        public void Execute_WithNewNotes_ChangesTheNotesOfTheTargetTopic()
//        {
//        }
        
        /// <summary>
        /// Creates a <see cref="AddTopicAction"/> for common action tests.
        /// </summary>
        protected override IAction CreateAction()
        {
            return new ChangeTopicAction( new Topic() ) { Name = "test123" };
        }
    }
}